package client;


import java.io.IOException;
import java.util.ArrayList;
import org.apache.log4j.Level;
import logger.LogSetup;
import utils.ClientConnectionUtil;
import utils.CommonUtil;
import app_kvClient.Constants;
import app_kvServer.KVServer;
import common.messages.KVMessage;
import common.messages.KVMessageImpl;

public class KVStore implements KVCommInterface {

	private LogSetup clientLogger; // client log
	private String serverAddress = "";
	private int serverPort = 0;	
	boolean serverNoResponsible=false;
	private ClientConnectionUtil connectionUtil; // Connection object
	private ArrayList<ClientMetadata> metadata;
	public String getServerAddress() {
		return serverAddress;
	}
	public void setServerAddress(String serverAddress) {
		this.serverAddress = serverAddress;
	}
	public int getServerPort() {
		return serverPort;
	}
	public void setServerPort(int serverPort) {
		this.serverPort = serverPort;
	}
	public void setInitialMetadata(){
		this.metadata=new ArrayList<ClientMetadata>();
		this.metadata.add(new ClientMetadata(serverAddress, serverPort,"0000000000000000000000" ,"ffffffffffffffffffffffffffffffff" ));
		
	}
	/**
	 * Initialize KVStore with address and port of KVServer
	 * @param address the address of the KVServer
	 * @param port the port of the KVServer
	 */
	public KVStore(String address, int port) {
		this(address,port,true);
		this.setInitialMetadata();
	}
	
	public KVStore(String address, int port, boolean loggingEnabled){
		this.serverAddress = address;
		this.serverPort = port;
		this.setInitialMetadata();
		connectionUtil = new ClientConnectionUtil();
		if(loggingEnabled)
			setupLogger(true);
		else
			setupLogger(false);
	}
	
	private void setupLogger(boolean loggingEnabled) {
		try {
			Level level = Level.ALL;
			if(!loggingEnabled)
				level = Level.OFF;
			clientLogger = new LogSetup(Constants.LOGGERS.CLIENT_LIBRARY_LOGGER, level);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public void connect() throws Exception {
		if(isConnectionEstablished())
			disconnect();
		try {
			boolean connectionSucceed = connectionUtil.initSocketConnection(serverAddress, serverPort);
			getServerResponse();
			clientLogger.infoLog(Constants.MESSAGES.CONNECTION_SUCCEEDED, null);
		} catch (Exception ex){
			clientLogger.infoLog(Constants.ERRORS.CONNECTION_NOT_SUCCEEDED, ex);
			throw ex;
		}
	}
	@Override
	public void disconnect() {
		try {
			if(connectionUtil.isConnectionEstablished()){
				connectionUtil.closeConnection();
				clientLogger.infoLog("successfully disconnected from the host",null);
			}
		} catch (IOException exc) {
			clientLogger.errorLog("Caught when tried to close the connection", exc);
		}
	}
	@Override
	public KVMessage put(String key, String value) throws Exception {
		KVMessageImpl message = new KVMessageImpl(key, value, KVMessage.StatusType.PUT);
		byte[] messageBytes = message.toBytes();
		KVMessage serverResponse = null;
		try {
			clientLogger.infoLog(message.toString(),null);
			connectionUtil.send(messageBytes);
			
		} catch (Exception ex) {
			clientLogger.infoLog(Constants.ERRORS.RESPONSE_ERROR, ex);
			throw ex;
		}
		//Check if the server is responsible for the request
		serverResponse = getServerResponse();
		getDecision(serverResponse, key);
		return serverResponse;
	}

	private void getDecision(KVMessage serverResponse, String key) throws Exception {
		String response = serverResponse.toString();
		serverResponsible(response);
		if(serverNoResponsible){
			updateMetadata(response);
			queryMetadata(key);
			try{connect();
				}
			catch(Exception ex){
			clientLogger.infoLog(Constants.ERRORS.RESPONSE_ERROR, ex);
			throw ex;
			}
		}
		
	}

	private void serverResponsible(String response) {
		if(response.contains("SERVER_NOT_RESPONSIBLE"))
			this.serverNoResponsible=true;
	}

	/**
	 * This method gets the server response and converts it into ASCII format
	 * 
	 * @exception IOException
	 *                when cannot connect successfully with the server
	 */
	private KVMessage getServerResponse() {
		byte[] responseBytes;
		KVMessage response = null;
		try {
			responseBytes = connectionUtil.receive();
			response = new KVMessageImpl(responseBytes);
			clientLogger.infoLog(response.toString(),null);
		} catch (IOException exc) {
			clientLogger.errorLog("An error occured while getting response from the server", exc);
			System.out.print(Constants.ERRORS.CONNECTION_NOT_SUCCEEDED);
		}
		return response;
	}
	
	@Override
	public KVMessage get(String key) throws Exception {
		KVMessageImpl message = new KVMessageImpl(key, "", KVMessage.StatusType.GET);
		byte[] messageBytes = message.toBytes();
		KVMessage serverResponse = null;
		try {
			connectionUtil.send(messageBytes);
			clientLogger.infoLog(message.toString(),null);
		} catch (Exception ex) {
			clientLogger.infoLog(Constants.ERRORS.RESPONSE_ERROR, ex);
			throw ex;
		}
		
		serverResponse = getServerResponse();
		getDecision(serverResponse, key);
		return serverResponse;
	}
	
	public boolean isConnectionEstablished() {
		return connectionUtil.isConnectionEstablished();
	}

	 public void updateMetadata(String response) {
		    String[] tokens = response.trim().split("###");
	    	String meta = tokens[2];
	    	String key =tokens[1];
	    	this.metadata=ClientMetadata.unmarshal(meta);
	    }
		
	public void queryMetadata(String key){
		String hashedKey=CommonUtil.hashString(key);
		for (ClientMetadata i:this.metadata) {
			if(KVServer.isInRange(hashedKey, i.getRangeStart(), i.getRangeEnd())){
				this.serverPort=i.getPort();
				this.serverAddress=i.getIpAddress();
				serverNoResponsible=false;
				break;
			}			
		}
		System.out.println("Server not responsible! Metadata updating...");
		System.out.println("Please re-type your command");
		
	}	
	
	 
	
}
